import java.awt.*;
import java.awt.event.*;
import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

import javax.swing.*;
import javax.swing.undo.*;

/** QuiverPanel is the content that lies within each tab in the frame of the main program
 *
 */
public class QuiverPanel extends JPanel implements MouseListener, MouseMotionListener {
    private static final long           serialVersionUID = 1L;
    public final Configuration          config = Configuration.getDefaultConfiguration();
    public Quiver                       quiver;

    /* node following the mouse when clicking a node to add a new one */
    private Node                        indicatedNode;
    private Arrow                       indicatedArrow;
    private Node                        heldNode, selectedNode, hoveredNode;
    private Set<Node>                   selectedNodes;
    private boolean                     nodeMoved, multipleNodesMoved,  arrowMoved;
    private Arrow                       selectedArrow;
    private LabelInput                  labelInput;
    private double                      oldXCoordinate, oldYCoordinate;
    private Arrow                       heldArrow;
    private ArrayList<Quiver>           history = new ArrayList<Quiver>();
    private UndoManager                 undoManager = new UndoManager();
    private int                         historyEntry = -1;
    private ArrayList<Representation>   representations = new ArrayList<Representation>();
    private Representation              activeRepr = null;
    private Relations                   relations;
    private Rectangle                   mouseSelectionRectangle;
    private Point                       mouseAnchorPoint;



    // START CONSTRUCTORS //
    public QuiverPanel(Quiver quiver) {// Constructor
        setLayout(null);

        this.quiver = quiver;
        setBackground(config.backgroundColor);
        setFocusable(true);

        addMouseListener(this);
        addMouseMotionListener(this);

//        addEvent();
        selectedNodes = new HashSet<Node>();

        relations = new Relations(quiver);

        undoManager.setLimit(config.undoSize);
        updateUndoRedoActions();
    }
    // END CONSTRUCTORS //



    // START UNDO/REDO EVENTS //

    private void updateUndoRedoActions() {
        Actions.undoAction.setEnabled(canUndo());
        Actions.redoAction.setEnabled(canRedo());
    }

    public void undo() {
        undoManager.undo();
        updateUndoRedoActions();
        repaint();
    }

    public void redo() {
        undoManager.redo();
        updateUndoRedoActions();
        repaint();
    }

    public boolean canUndo() {
        return undoManager.canUndo();
    }

    public boolean canRedo() {
        return undoManager.canRedo();
    }
    // END UNDO/REDO EVENTS //



    // START MOVING //
    public void moveAutoArrangeQuiverToHeldNode() {//Auto arranges all nodes in quiver relative to held node
        Set<Node> nodes = quiver.getNodes();
        int numNodes = nodes.size();
        for (Node node1 : nodes) {
            if (node1 == heldNode) {
                continue;
            }
            double fx = 0, fy = 0;
            for (Node node2 : nodes) {
                if (node1 == node2) {
                    continue;
                }
                // should preferred distance increase with number of nodes?
                double force = -0.5
                        * config.hookesConstant
                        * (config.preferredNodeDistance + 5 * numNodes - node1
                        .distance(node2));
                double angle = node1.getAngle(node2);
                fx += force * Math.cos(angle);
                fy += force * Math.sin(angle);
            }
            Point2D point = node1.getPoint();
            node1.moveTo(point.getX() + fx, point.getY() + fy);
        }
        repaint();
    }

    //Moves only the currently held node
    private void moveHeldNode(MouseEvent e) {
        Node node = quiver.getNode(e.getPoint(), heldNode);
        if (node != null) {
            heldNode.moveTo(node.getPoint());
        } else if (e.isShiftDown()) {
            heldNode.snapToGrid(e.getX(), e.getY());
        } else {
            heldNode.moveTo(e.getX(), e.getY());
        }
        nodeMoved = true;
    }

    //Move the currently held Arrow
    private void moveHeldArrow(MouseEvent e) {
        heldArrow.move(e.getX() - oldXCoordinate, e.getY() - oldYCoordinate);
        storeMouseCoordinates(e);
        arrowMoved = true;
    }

    //Move all selected nodes relative to one held node in the selection
    private void moveSelectedNodes(MouseEvent e) {
        double dx = e.getX() - oldXCoordinate;
        double dy = e.getY() - oldYCoordinate;
        for (Node node : selectedNodes) {
            node.move(dx, dy);
        }
        storeMouseCoordinates(e);
        multipleNodesMoved = true;
    }
    // END MOVING //



    // START PAINTING //
    public void paintGrid(Graphics2D g) {//Draws up the grid one draws on top of
        double width = getWidth();
        double height = getHeight();
        g.setPaint(config.gridColor);
        for (double x = 0; x < width; x += config.gridSize) {
            g.drawLine((int) x, 0, (int) x, (int) height);
        }
        for (double y = 0; y < height; y += config.gridSize) {
            g.drawLine(0, (int) y, (int) width, (int) y);
        }
    }

    public void paint(Graphics g) {//Draws/refreshes all graphics
        Graphics2D g2 = (Graphics2D) g;
        super.paint(g2);
        paintMouseSelectionRectangle(g2);
        if (config.gridVisible) {
            paintGrid(g2);
        }
        g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                RenderingHints.VALUE_ANTIALIAS_ON);

        for (Node node : quiver.getNodes()) {
            if (activeRepr != null) {
                node.paint(g2, activeRepr);
            } else {
                node.paint(g2);
            }
        }
        for (Arrow arrow : quiver.getArrows()) {
            if (arrow.getFromNode() == arrow.getToNode()
                    || !arrow.getFromNode().isClose(arrow.getToNode())) {
                if (activeRepr != null) {
                    arrow.paint(g2, activeRepr);
                } else {
                    arrow.paint(g2);
                }
            }
        }
        if (indicatedArrow != null && selectedNode != null
                && !indicatedNode.isClose(selectedNode)) {
            indicatedArrow.paint(g2);
            indicatedNode.paint(g2);
        }
        if (labelInput != null){
            labelInput.repaint();
        }
    }

    public void paintMouseSelectionRectangle(Graphics2D g) {//Draws a rectangle from mouse points given by mouse pressed and released
        super.paintComponent(g);
        if (mouseSelectionRectangle!=null){
            g.setColor(new Color(128, 128, 255));
            g.fill(mouseSelectionRectangle);
//            g.draw(mouseSelectionRectangle); // Don't think this is necessary
        }
    }

    public void clear() {
        quiver.clear();
        selectedNode = null;
        selectedArrow = null;
        hoveredNode = null;
        heldNode = null;
        heldArrow = null;
        indicatedNode = null;
        indicatedArrow = null;
        updateUndoRedoActions();
//        addEvent();
        repaint();
    }
    // END PAINTING //



    // START MOUSE EVENTS //
    //Holds coordinates of given mouse event
    private void storeMouseCoordinates(MouseEvent e) {
        oldXCoordinate = e.getX();
        oldYCoordinate = e.getY();
    }

    @Override
    public void mouseClicked(MouseEvent e) {
        selectLabel(null);
        Node node = quiver.getNode(e.getPoint());
        Named label = quiver.getLabel(e.getPoint());
        Arrow arrow = quiver.getArrow(e.getPoint());

        switch (e.getButton()) {
            case MouseEvent.BUTTON1:// Left mouse button clicked
                if (e.isShiftDown()) {
                    selectOrDeselectNodes(node);
                    repaint();
                } else {
                    if (selectedNode == null) { //All options for when a node is clicked and selected
                        if (node != null) { //Selects and indicates (used for drawing arrows) if mouse pointer has clicked existing node.
                            selectNode(node);
                            indicateNode(node);
                        } else if (arrow != null && e.isControlDown()) { // reverse direction of arrow
                            undoManager.addEdit(new FlipArrow(arrow));
                            updateUndoRedoActions();
//                            repaint();
//                            addEvent();
                        } else if (label != null) {
                            selectLabel(label);
                        } else if (arrow != null) { //Seems unused, selected arrow is never used for anything
                            selectArrow(arrow);
                        } else {                               //The mouse pointer har clicked on empty space, node added in it's place.
//                            quiver.addNode(e.getX(), e.getY());
                            undoManager.addEdit(new AddNode(e.getX(), e.getY()));
                            repaint();
                            selectArrow(null);
                            updateUndoRedoActions();
//                            addEvent();
                        }
                    } else {
                        if (node != null) { //Arrow is drawn between previously selected node and currently selected node
//                            quiver.addArrow(selectedNode, node);
                            undoManager.addEdit(new AddArrow(selectedNode, node));
                            updateUndoRedoActions();
//                            addEvent();
                        } else if (label != null) {
                            selectLabel(label);
                        } else {//Arrow is drawn between previously selected node and a new node, new node added on current mouse pointer.

                            undoManager.addEdit(new AddNodeAndArrow(e.getX(), e.getY(), selectedNode));
                            updateUndoRedoActions();
//                            selectArrow(quiver.addArrow(selectedNode, newNode)); //
//                            addEvent();
                        }
                        selectNode(null);
                    }
                }

                break;
            case MouseEvent.BUTTON3: //Right mouse button clicked
                if (heldNode != null) {
                    heldNode.cancelMove();
                    holdNode(null);
                } else if (node != null) { //Deletes right clicked node
                    deleteNode(node);
                    break;
                } else if (arrow != null) {//Deletes right clicked arrow
                    quiver.removeArrow(arrow);
                    updateUndoRedoActions();
//                    addEvent();
                    repaint();
                }
                if (selectedArrow != null) {//Unselects selected arrow
                    selectArrow(null);
                }
                if (selectedNode != null) {//Unselects selected node
                    selectNode(null);
                }
                if (!selectedNodes.isEmpty()) {//If mouse right clicked and nodes are selected, unselects
                    clearSelectedNodes();
                }
                selectLabel(null);//Deselects label
        }
    }

    @Override
    public void mouseEntered(MouseEvent e) {
        // TODO Auto-generated method stub

    }

    @Override
    public void mouseExited(MouseEvent e) {
        // TODO Auto-generated method stub

    }

    @Override
    public void mousePressed(MouseEvent e) {
        requestFocus();
        selectLabel(null);
        mouseAnchorPoint = e.getPoint();
        mouseSelectionRectangle = new Rectangle(mouseAnchorPoint);//Prepares for rectangle selection, coordinates given in mouseDragged and drawn in paintMouseSelectionRectangle
        storeMouseCoordinates(e);
        switch (e.getButton()) {
            case MouseEvent.BUTTON1:
                if (heldNode == null) { //Holds node if not already held and mouse pointer rests over node
                    Node node = quiver.getNode(e.getPoint());
                    if (node != null) {
                        holdNode(node);
                    }
                }
                if (heldArrow == null && heldNode == null) {// //Holds arrow if not already held and mouse pointer rests over arrow
                    Arrow arrow = quiver.getArrow(e.getPoint());
                    if (arrow != null) {
                        holdArrow(arrow);
                    }
                }
                break;
        }
    }

    @Override
    public void mouseReleased(MouseEvent e) {
        switch (e.getButton()) {
            case MouseEvent.BUTTON1:
                if (heldNode != null) {
                    if (nodeMoved) {
                        Node node = quiver.getNode(e.getPoint(), heldNode);
                        if (node != null) {
                            quiver.copyArrows(heldNode, node);
                            undoManager.addEdit(new RemoveNode(heldNode));
//                            quiver.removeNode(heldNode);
                        }
                        updateUndoRedoActions();
//                        addEvent();
                    }
                    holdNode(null);
                }
                if (heldArrow != null) {
                    if (arrowMoved) {
                        updateUndoRedoActions();
//                        addEvent();
                    }
                    holdArrow(null);
                }
                if (!selectedNodes.isEmpty()) {
                    if (multipleNodesMoved) {
                        updateUndoRedoActions();
//                        addEvent();
                    }
                }
                break;
        }
        // Removes mouseSelectionRectangle when mouse is released
        mouseSelectionRectangle = null;
        repaint();
    }

    @Override
    //Controls possible methods fired when you drag the mouse pointer
    public void mouseDragged(MouseEvent e) {
        final boolean holdsNode = heldNode != null;
        final boolean holdsArrow = heldArrow != null;
        final boolean holdsRectangle = mouseSelectionRectangle != null;

        final boolean isMultipleNodesMove = holdsNode && !selectedNodes.isEmpty() && selectedNodes.contains(heldNode);
        final boolean isSingleNodeMove = holdsNode && (nodeMoved || !heldNode.isClose(e.getPoint()));
        final boolean isArrowMove = holdsArrow && (arrowMoved || !heldArrow.isClose(e.getPoint()));
        final boolean isAreaSelection = !holdsNode && !holdsArrow && holdsRectangle;

        if (isMultipleNodesMove) {
            moveSelectedNodes(e);
        } else if (isSingleNodeMove) {
            moveHeldNode(e);
        } else if (isArrowMove) {
            moveHeldArrow(e);
        } else if (isAreaSelection) {
            drawSelectionRectangle(e);
        }

        repaint();
    }

    //Draws rectangle and selects nodes within the rectangle
    private void drawSelectionRectangle(MouseEvent e) {
        mouseSelectionRectangle.setBounds(Math.min(mouseAnchorPoint.x, e.getX()), Math.min(mouseAnchorPoint.y, e.getY()),
                Math.abs(e.getX() - mouseAnchorPoint.x), Math.abs(e.getY() - mouseAnchorPoint.y));
        selectNodes(mouseSelectionRectangle);
    }

    @Override
    public void mouseMoved(MouseEvent e) {
        Node node = quiver.getNode(e.getPoint());
        if (node != null) {
            hoverNode(node);
        } else if (hoveredNode != null) {
            hoverNode(null);
        }
        if (indicatedNode != null && indicatedArrow != null) {
            if (hoveredNode != null && indicatedNode.isClose(hoveredNode)) {
                indicatedNode.moveTo(hoveredNode.getPoint());
            } else {
                indicatedNode.moveTo(e.getPoint());
            }
            indicatedArrow.update();
            repaint();
        }
    }
    // END MOUSE EVENTS //



    // START NODE ACTIONS //
    public void deleteNode(Node node) {
        Set<Relation> deletees = getRelationsOnNamedDeletion(node);
        if (!deletees.isEmpty()) {
            if (showConfirmDeleteRelationDialog() == JOptionPane.NO_OPTION) {
                return;
            }
        }
        for (Relation relation : deletees) {
            removeRelation(relation);
        }
        QuiverFrame.getInstance().relationListModel.update();

        undoManager.addEdit(new RemoveNode(node));
//        quiver.removeNode(node);
        if (indicatedNode != null) {
            undoManager.addEdit(new RemoveNode(indicatedNode));
//            quiver.removeNode(indicatedNode);
        }
        updateUndoRedoActions();
    }

    public void deleteSelectedNodes() {
        Set<Relation> deletees = new HashSet<Relation>();
        for (Node node : selectedNodes) {
            deletees.addAll(getRelationsOnNamedDeletion(node));
        }
        if (!deletees.isEmpty()) {
            if (showConfirmDeleteRelationDialog() == JOptionPane.NO_OPTION) {
                return;
            }
        }
        for (Relation relation : deletees) {
            removeRelation(relation);
        }
        QuiverFrame.getInstance().relationListModel.update();
        
        for (Node node : selectedNodes) {
            deleteNode(node);
        }
        clearSelectedNodes();
    }

    public void clearSelectedNodes() { //Clears and unhighlights set of selected nodes
        for (Node n : selectedNodes) {
            n.setHighlighted(false);
        }
        selectedNodes.clear();
    }

    private void selectOrDeselectNodes(Node node) { //Selects and highlights input node if not selected and vice versa
        if (selectedNodes != null && node != null)  {
            if (selectedNodes.contains(node))  {
                node.setHighlighted(false);
                selectedNodes.remove(node);
            } else {
                node.setHighlighted(true);
                selectedNodes.add(node);
            }
        }
    }

    public void indicateNode(Node node) {
        if (node == indicatedNode) {
            return;
        }
        if (node != null) {
            indicatedNode = new Node(node.getPoint().getX(), node.getPoint()
                    .getY());
            indicatedNode.setIndicated(true);
            indicatedArrow = new Arrow(selectedNode, indicatedNode);
        } else if (selectedNode != null) {
            indicatedNode = null;
            indicatedArrow = null;
        }
    }

    public void selectNode(Node node) {
        if (node == selectedNode) {
            return;
        }
        if (node != null) {
            selectedNode = node;
            selectedNode.setSelected(true);
            repaint();
        } else if (selectedNode != null) {
            selectedNode.setSelected(false);
            selectedNode = null;
            repaint();
        }
    }

    /** This is a method that selects and highlights all the nodes that lies within the borders of a given rectangle
     * @param selectionBox The selectionbox is merely a rectangle drawn by the user to select nodes
     *
     * */
    private void selectNodes(Rectangle selectionBox) {
        for (Node node : quiver.getNodes()) {
            if (selectionBox.contains(node.getPoint())) {
                selectedNodes.add(node);
                node.setHighlighted(true);
            } else {
                selectedNodes.remove(node);
                node.setHighlighted(false);
            }
        }
    }

    public void holdNode(Node node) {
        if (node == heldNode) {
            return;
        }
        if (node != null) {
            heldNode = node;
            heldNode.setHeld(true);
            repaint();
        } else if (heldNode != null) {
            heldNode.setHeld(false);
            heldNode = null;
            nodeMoved = false;
            repaint();
        }
    }

    public void hoverNode(Node node) {
        if (node == hoveredNode) {
            return;
        }
        if (node != null) {
            hoveredNode = node;
            hoveredNode.setHovered(true);
            repaint();
        } else if (hoveredNode != null) {
            hoveredNode.setHovered(false);
            hoveredNode = null;
            repaint();
        }
    }
    // END NODE ACTIONS //



    // START ARROW ACTIONS //
    public void selectArrow(Arrow arrow) {
        if (arrow == selectedArrow) {
            return;
        }
        if (selectedArrow != null) {
            selectedArrow.setSelected(false);
            selectedArrow = null;
            repaint();
        }
        if (arrow != null) {
            selectedArrow = arrow;
            selectedArrow.setSelected(true);
            repaint();
        }
    }

    public void holdArrow(Arrow arrow) {
        if (arrow == heldArrow) {
            return;
        }
        if (arrow != null) {
            heldArrow = arrow;
            heldArrow.setHeld(true);
            repaint();
        } else if (heldArrow != null) {
            heldArrow.setHeld(false);
            heldArrow = null;
            arrowMoved = false;
            repaint();
        }
    }
    // END ARROW ACTIONS //



    // START LABEL ACTIONS //
    public void selectLabel(Named object) {
        if (labelInput != null){
            labelInput.write();
            remove(labelInput);
        }
        if (object == null) {
            labelInput = null;
            return;
        }
        if (object instanceof Arrow && activeRepr != null) {
            labelInput = new MatrixInput(activeRepr, (Arrow) object);
        } else if (object instanceof Node && activeRepr != null) {
            labelInput = new DimensionInput(activeRepr, (Node) object);
        } else {
            labelInput = new NameInput(object);
        }
        add(labelInput);
    }
    // END LABEL ACTIONS //



    // START REPRESENTATIONS //
    public void addRepresentation() {
        addRepresentation("");
    }

    public void addRepresentation(String name) {
        addRepresentation(new Representation(quiver));
    }

    public void addRepresentation(Representation representation) {
        representations.add(representation);
        repaint();
    }

    public Representation getActiveRepresentation() {
        return activeRepr;
    }

    public void setActiveRepresentation(Representation repr) {
        activeRepr = repr;
        repaint();
    }

    public ArrayList<Representation> getRepresentations() {
        return representations;
    }
    // END REPRESENTATIONS //



    // START RELATIONS //
    public boolean addRelation(String relationString) {
        Relation relation = quiver.createRelation(relationString);
        if (relation != null && !relations.contains(relation)) {
            relations.add(relation);
            QuiverFrame.getInstance().relationListModel.update();
            return true;
        }
        return false;
    }

    public void removeRelation(Relation relation) {
        relations.remove(relation);
    }

    public Relations getRelations() {
        return relations;
    }
    
    private Set<Relation> getRelationsOnNamedDeletion(Named element) {
        Set<Relation> deletees = new HashSet<Relation>();
        for (Relation relation : relations.getRelations()) {
            if (relation.contains(element)) {
                deletees.add(relation);
            }
        }
        return deletees;
    }
    // END RELATIONS //
    // START DIALOGS //
    private int showConfirmDeleteRelationDialog() {
        return JOptionPane.showConfirmDialog(QuiverFrame.getInstance(), "You are about to delete an\nelement" +
                " that exists in a relation.\nContinue?", "Deleting part of a relation",
                JOptionPane.YES_NO_OPTION);
    }
    // END DIALOGS //

    // START UNDO/REDO INNER CLASSES //
    class AddNode extends AbstractUndoableEdit {


        private Node node;

        public AddNode(double x, double y) {
            System.out.println("addnode ctor");
            this.node = quiver.addNode(x, y);
        }

        @Override
        public void undo() throws CannotUndoException {
            System.out.println("addnode undo");
            super.undo();
            quiver.removeNode(node);
        }

        @Override
        public void redo() throws CannotRedoException {
            System.out.println("addnode redo");
            super.redo();
            quiver.addNode(node);
        }

        public Node getNode() {
            return node;
        }
    }
    
    class AddNodeAndArrow extends CompoundEdit {
        private Node node;
        private Arrow arrow;
        
        public AddNodeAndArrow(double x, double y, Node start) {
            AddNode an = new AddNode(x,y);
            addEdit(an);
            addEdit(new AddArrow(start, an.getNode()));

        }
    }
    
    class AddArrow extends AbstractUndoableEdit {
        private Arrow arrow;
        
        public AddArrow(Node start, Node end) {
            System.out.println("addarrow ctor");
            arrow = quiver.addArrow(start, end);
        }

        @Override
        public void undo() throws CannotUndoException {
            super.undo();
            quiver.removeArrow(arrow);
        }

        @Override
        public void redo() throws CannotRedoException {
            super.redo();
            quiver.addArrow(arrow);
        }
    }

    class RemoveArrow extends AbstractUndoableEdit {
        private Arrow arrow;
        
        public RemoveArrow(Arrow arrow) {
            System.out.println("removearrow ctor");
            quiver.removeArrow(arrow);
            this.arrow = arrow;
        }

        @Override
        public void undo() throws CannotUndoException {
            super.undo();
            quiver.addArrow(arrow);
        }

        @Override
        public void redo() throws CannotRedoException {
            super.redo();
            quiver.addArrow(arrow);
        }
    }
    class RemoveNode extends AbstractUndoableEdit {

        private Node node;

        public RemoveNode(Node node) {
            quiver.removeNode(node);
            this.node = node;
        }

        @Override
        public void undo() throws CannotUndoException {
            super.undo();
            quiver.addNode(node);
        }
        @Override
        public void redo() throws CannotRedoException {
            super.redo();
            quiver.removeNode(node);
        }

    }
    class MoveNode extends AbstractUndoableEdit {
        private Node node;

        private Point2D point;

        public MoveNode(Node node, Point2D point) {
            this.node = node;
            this.point = node.getPoint();
            node.moveTo(point);
        }
        @Override
        public void undo() throws CannotUndoException {
            super.undo();

        }

    }

    
    
    class FlipArrow extends AbstractUndoableEdit {
        private Arrow arrow;
        
        public FlipArrow(Arrow arrow) {
            this.arrow = arrow;
            arrow.flip();
        }

        @Override
        public void undo() throws CannotUndoException {
            super.undo();
            arrow.flip();
        }

        @Override
        public void redo() throws CannotRedoException {
            super.redo();
            arrow.flip();
        }
    }
    // END UNDO/REDO INNER CLASSES //
}
